<template>
  <div class="fabric-board">
    <div class="tools">
      <input type="color" v-model="color" @change="changeColor" />
      <button :disabled="!canUndo" @click="historyState(-1)">撤销</button>
      <button :disabled="!canRedo" @click="historyState(1)">恢复</button>
      <button :disabled="scale >= 1.6" @click="changeScale(0.2)">放大</button>
      <button :disabled="scale <= 0.6" @click="changeScale(-0.2)">缩小</button>
      <button @click="clearCanvas">清空</button>
      <button @click="exportCanvas">导出</button>
    </div>
    <div class="tools">
      <span>自由绘制：</span>
      <label v-for="radio in radioList" :key="radio.value">
        <input type="radio" :value="radio.value" v-model="currentType" />
        {{ radio.label }}
      </label>
    </div>
    <div :style="canvasStyle">
      <canvas id="canvas" />
    </div>
    <p style="margin-top: 6px">
      {{ imgLoading ? '图片加载中……' : '图片加载完成' }}
    </p>
    <p>scale: {{ scale }} -- objects: {{ objects }}</p>
  </div>
</template>

<script>
import { fabric } from 'fabric'
const IMGURL =
  'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg'
export default {
  name: 'FabricBoard',
  data() {
    return {
      canvas: null,
      canvasObject: null,
      textboxObject: null,
      loadImg: null,
      width: 800,
      height: 500,
      scale: 1,
      color: '#ff0000',
      brushSize: 1,
      fontSize: 16,
      imgLoading: true,
      currentType: '',
      canvasState: [],
      stateIndex: -1,
      isDrawing: false,
      downPoint: null,
      radioList: [
        { label: '控制', value: 'default' },
        { label: '画笔', value: 'brush' },
        { label: '对号', value: 'correct' },
        { label: '错号', value: 'wrong' },
        { label: '矩形', value: 'rect' },
        { label: '椭圆', value: 'ellipse' },
        { label: '文本', value: 'textbox' },
      ],
    }
  },
  computed: {
    objects() {
      return this.canvas?._objects.length || 0
    },
    canUndo() {
      return this.stateIndex > 0
    },
    canRedo() {
      return this.stateIndex < this.canvasState.length - 1
    },
    canvasStyle() {
      return {
        border: '2px dashed red',
        backgroundImage: `url("${IMGURL}")`,
        backgroundSize: '100% 100%',
      }
    },
  },
  watch: {
    currentType(val) {
      this.canvas.selection = val === 'default'
      this.canvas.isDrawingMode = val === 'brush'
      this.canvas.freeDrawingBrush.color = this.color
      this.canvas.freeDrawingBrush.width = this.brushSize
      this.clearTextbox()
      this.setAllSelectable()
      // 取消所有已选中的对象
      this.canvas.discardActiveObject()
      this.canvas.renderAll()
    },
  },
  mounted() {
    this.canvas = new fabric.Canvas('canvas')
    this.currentType = 'default'
    this.setBgImg()
    this.setDeleteControl()

    this.canvas.on('mouse:down', this.canvasMoseDown)
    this.canvas.on('mouse:move', this.canvasMoseMove)
    this.canvas.on('mouse:up', this.canvasMoseUp)
    this.canvas.on('object:modified', this.saveCanvasState)
  },
  methods: {
    setBgImg() {
      this.imgLoading = true
      fabric.Image.fromURL(
        IMGURL,
        (img) => {
          this.loadImg = img
          this.imgLoading = false
          this.height = (img.height / img.width) * this.width
          this.changeScale()
          this.saveCanvasState()
        },
        { crossOrigin: 'anonymous' }
      )
    },

    setDeleteControl() {
      // 创建删除图片元素
      let deleteImg = document.createElement('img')
      // eslint-disable-next-line prettier/prettier
      deleteImg.src = 'data:image/svg+xml,%3C%3Fxml version=\'1.0\' encoding=\'utf-8\'%3F%3E%3C!DOCTYPE svg PUBLIC \'-//W3C//DTD SVG 1.1//EN\' \'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\'%3E%3Csvg version=\'1.1\' id=\'Ebene_1\' xmlns=\'http://www.w3.org/2000/svg\' xmlns:xlink=\'http://www.w3.org/1999/xlink\' x=\'0px\' y=\'0px\' width=\'595.275px\' height=\'595.275px\' viewBox=\'200 215 230 470\' xml:space=\'preserve\'%3E%3Ccircle style=\'fill:%23F44336;\' cx=\'299.76\' cy=\'439.067\' r=\'218.516\'/%3E%3Cg%3E%3Crect x=\'267.162\' y=\'307.978\' transform=\'matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)\' style=\'fill:white;\' width=\'65.545\' height=\'262.18\'/%3E%3Crect x=\'266.988\' y=\'308.153\' transform=\'matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)\' style=\'fill:white;\' width=\'65.544\' height=\'262.179\'/%3E%3C/g%3E%3C/svg%3E'

      // 删除按钮控件
      const deleteControl = new fabric.Control({
        x: 0.5,
        y: -0.5,
        offsetY: -16,
        offsetX: 16,
        cursorStyle: 'pointer',
        cornerSize: 24,
        mouseUpHandler: (eventData, transform) => {
          const canvas = transform.target.canvas
          const activeObject = canvas.getActiveObjects()
          if (activeObject) {
            activeObject.map((item) => canvas.remove(item))
            canvas.requestRenderAll()
            canvas.discardActiveObject()
            this.saveCanvasState()
          }
        },
        render: function (ctx, left, top, styleOverride, fabricObject) {
          const size = this.cornerSize
          ctx.save()
          ctx.translate(left, top)
          ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle))
          ctx.drawImage(deleteImg, -size / 2, -size / 2, size, size)
          ctx.restore()
        },
      })
      fabric.Object.prototype.controls.deleteControl = deleteControl
      fabric.Textbox.prototype.controls.deleteControl = deleteControl
    },

    changeColor() {
      this.canvas.freeDrawingBrush.color = this.color
    },

    changeScale(step = 0) {
      this.scale = (this.scale * 10 + step * 10) / 10
      this.canvas.setZoom(this.scale)
      this.canvas.setWidth(this.width * this.scale)
      this.canvas.setHeight(this.height * this.scale)
    },

    canvasMoseDown(e) {
      this.isDrawing = true
      this.downPoint = e.absolutePointer
      const params = {
        top: e.absolutePointer.y,
        left: e.absolutePointer.x,
        fill: 'transparent',
        stroke: this.color,
        strokeUniform: true, // 限制边框宽度缩放
        selectable: false,
      }
      switch (this.currentType) {
        case 'rect':
          this.canvasObject = new fabric.Rect({
            ...params,
            width: 0,
            height: 0,
          })
          this.canvas.add(this.canvasObject)
          break
        case 'ellipse':
          this.canvasObject = new fabric.Ellipse({
            ...params,
            rx: 0,
            ry: 0,
          })
          this.canvas.add(this.canvasObject)
          break
        case 'textbox':
          this.clearTextbox()
          this.textboxObject = new fabric.Textbox('', {
            ...params,
            fill: this.color,
            fontSize: this.fontSize,
            strokeWidth: 0.1,
          })
          this.canvas.add(this.textboxObject)
          this.textboxObject.enterEditing()
          break
        case 'correct':
        case 'wrong':
          this.canvasObject = new fabric.Path()
          this.canvas.add(this.canvasObject)
          break

        default:
          break
      }
    },

    canvasMoseMove(e) {
      if (!this.isDrawing || !this.canvasObject) return
      const dx = Math.min(this.downPoint.x, e.absolutePointer.x)
      const dy = Math.min(this.downPoint.y, e.absolutePointer.y)
      const px = Math.max(this.downPoint.x, e.absolutePointer.x)
      const py = Math.max(this.downPoint.y, e.absolutePointer.y)
      const pathParams = {
        fill: 'transparent',
        stroke: this.color,
        strokeWidth: 1,
        strokeUniform: true, // 限制边框宽度缩放
        selectable: false,
      }
      switch (this.currentType) {
        case 'rect': {
          // 矩形参数计算
          const top = Math.min(dy, py)
          const left = Math.min(dx, px)
          const width = Math.abs(dx - px)
          const height = Math.abs(dy - py)
          this.canvasObject.set({ top, left, width, height })
          break
        }
        case 'ellipse': {
          const rx = Math.abs(dx - px) / 2
          const ry = Math.abs(dy - py) / 2
          const top = py > dy ? dy : dy - ry * 2
          const left = px > dx ? dx : dx - rx * 2
          this.canvasObject.set({ rx, ry, top, left })
          break
        }
        case 'correct': {
          const path = `M ${dx} ${dy} L ${dx + (px - dx) / 5} ${
            dy + (px - dx) / 3
          } L ${dx + (px - dx)} ${dy - (px - dx) / 8}`
          this.canvas.remove(this.canvasObject)
          this.canvasObject = new fabric.Path(path, pathParams)
          this.canvas.add(this.canvasObject)
          break
        }
        case 'wrong': {
          const path = `M ${dx} ${dy} L ${px} ${
            dy + (px - dx)
          } M ${px} ${dy} L ${dx} ${dy + (px - dx)}`
          this.canvas.remove(this.canvasObject)
          this.canvasObject = new fabric.Path(path, pathParams)
          this.canvas.add(this.canvasObject)
          break
        }

        default:
          break
      }
      this.canvas.renderAll()
      this.canvasObject.setCoords()
    },

    canvasMoseUp(e) {
      // 保存历史数据
      if (!['default', 'textbox'].includes(this.currentType)) {
        this.saveCanvasState()
      }
      // 原地点击不创建对象
      const isClick =
        JSON.stringify(this.downPoint) === JSON.stringify(e.absolutePointer)
      if (
        ['rect', 'ellipse', 'correct', 'wrong'].includes(this.currentType) &&
        isClick
      ) {
        this.canvas.remove(this.canvasObject)
        this.deleteCanvasState()
      }
      this.isDrawing = false
      this.downPoint = null
      this.canvasObject = null
    },

    saveCanvasState() {
      const canvasAsJson = JSON.stringify(this.canvas.toJSON())
      this.canvasState.splice(this.stateIndex + 1)
      this.canvasState.push(canvasAsJson)
      this.stateIndex = this.canvasState.length - 1
      console.log('保存历史记录', this.stateIndex)
    },

    deleteCanvasState(num = 1) {
      this.canvasState.splice(this.canvasState.length - num, num)
      this.stateIndex = this.canvasState.length - 1
    },

    setAllSelectable() {
      this.canvas.forEachObject((v) => {
        v.set('selectable', this.currentType === 'default')
      })
    },

    historyState(step) {
      if (this.canUndo || this.canRedo) {
        this.currentType !== 'default' && this.clearTextbox()
        const index = this.stateIndex + step
        this.canvas.renderOnAddRemove = false
        this.canvas.loadFromJSON(this.canvasState[index], () => {
          this.stateIndex = index
          this.canvas.renderOnAddRemove = true
          this.setAllSelectable()
          this.canvas.renderAll()
        })
      }
    },

    clearTextbox() {
      if (!this.textboxObject) return
      this.textboxObject.exitEditing()
      this.textboxObject.text === '' && this.canvas.remove(this.textboxObject)
      this.textboxObject = null
    },

    clearCanvas() {
      this.canvas.forEachObject((obj) => this.canvas.remove(obj))
      this.canvasState.splice(1)
      this.stateIndex = 0
    },

    exportCanvas() {
      // 先插入背景图再生成图片
      this.canvas.setBackgroundImage(
        this.loadImg,
        this.canvas.renderAll.bind(this.canvas),
        {
          // 将背景图的宽高设置成画布的宽高
          scaleX: this.width / this.loadImg.width,
          scaleY: this.height / this.loadImg.height,
        }
      )
      const dataURL = this.canvas.toDataURL({
        format: 'png',
        multiplier: 1 / this.scale,
      })
      const link = document.createElement('a')
      link.download = 'canvas.png'
      link.href = dataURL
      document.body.appendChild(link)
      link.click()
      document.body.removeChild(link)
    },
  },
}
</script>

<style lang="less" scoped>
.fabric-board {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 10px;
  .tools {
    display: flex;
    align-items: center;
  }
  .tools > * {
    margin: 5px;
  }
}
</style>
